#!/usr/bin/python
#-*-coding: utf-8 -*-

u"""
:name: cmake
:author: ANONYMOUS
:license: UNKNOWN
"""
__all__ = [u'HandlerPart']

########## DO NOT CHANGE THESE LINES ###########################################
import sys
from pylot.core import (ENCODING, Path)
try : __file__
except NameError : co = sys._getframe(0).f_code ; __file__ = co.co_filename
ufile = __file__.decode(ENCODING)
if ufile.strip() : PLUGIN_PATH = Path(ufile).abspath().parent
else : PLUGIN_PATH = Path(u'.').abspath()
########## DO NOT CHANGE THESE LINES ###########################################

import copy
from pylot.core import (check_type, ENCODING, Path, hardbook, unicode2,
                           CommandGroup, ShellCmds, Identity, is_iterable,
                           ShellScriptWriter, CREATED_PATHS, Utils, mkdtempu,
                           GroupBook, merge_command_groups, Datum, regexpr,
                           Book)
from cmakeoption import (CMakeOptionBook, CMakeOption)
from libcmake import cg_make
from pylot.interfaces import (HandlerInterface)

class HandlerPart (HandlerInterface):
  u"""
  This handler provides methods to configure sources using cmake and build
  them using make. 
  Additional options are available in order to customize
  configuration.
  
  This class provides a pathbook containings these entries :
    - src: directory containing source files (.cxx, .tex, ...)
    - build: last build directory
    - builds: directory containing all project build directories
    - cmakeoptions: directory containing saved CMake options
  """

  def __init__ (self, plugin_path=None, plugin_id=None):
    u"""
    See :class:`~pylot.interfaces.HandlerInterface`
    """
    if plugin_path is None : plugin_path = PLUGIN_PATH
    HandlerInterface.__init__(self, plugin_path, plugin_id)

    self.addOptions(u'conf.csv', main=True)
    self.pathbook = self.opt[u'pathbook']
    self.created_paths = CREATED_PATHS
    self._cleancache = self.opt[u'general'][u'cleancache']
    self._debug_mode = self.opt[u'general'][u'debug_mode']

    identity = Identity(u'cmake_options', group=u'cmake')
    self._cmake_options = CMakeOptionBook(identity=identity)

    identity = Identity(u'cmake_options', group=u'cmake')
    self.options_book_expert = CMakeOptionBook(identity=identity)

    identity = Identity(u'cmake_options_expert', group=u'cmake')
    self.options_book = CMakeOptionBook(identity=identity)

    self.default_cmake_book = CMakeOptionBook()

    self.__scansrcpaths = set([])

    self.bash = ShellCmds()

    self.pathbook[u'src'] = Path(u'.').abspath()
    self.pathbook[u'builds'] = Path(hardbook(u'pylotsrc').parent, u'pylot-builds')
    self.pathbook[u'cmakeoptions'] = hardbook(u'userconf', u'cmakeoptions')

    groupbook = self.findFile(u'groupbook.csv')
    identity = Identity(u'groupbook', Path, group=u'group',
      tooltip=u'group description database', alias=u'Group Description Book')
    self.groupbook = GroupBook(identity=identity)
    self.groupbook.importfile(groupbook, fmt=u'csv')

  def _setcmake_options (self, value):
    self._cmake_options = value

  def _setcleancache(self, value):
    self._cleancache.value = value

  def _setdebug_mode(self, value):
    self._debug_mode.value = value

  cmake_options = property(fget=lambda self:self._cmake_options,
                           fset=_setcmake_options)
  cleancache = property(fget=lambda self:self._cleancache.value,
                        fset=_setcleancache)
  debug_mode = property(fget=lambda self:self._debug_mode.value,
                        fset=_setdebug_mode)

  def findMainProgramName(self, cmakelists):
    u"""Je cherche dans le CMakeList du dossier projet, le nom de l'exécutable"""
    main_program_name = u'a.out'
    for l in open(cmakelists, u'r'):
      if l.find(u"ADD_EXECUTABLE") >= 0:
        main_program_name = l.split(u'(')[1].strip().split(u' ')[0].strip(u' ./')
        reg = regexpr.co_cmake_varname.search(main_program_name)
        if reg : #not hard coided, a CMake variable is used for project name
          cmake_var = reg.group()[2:-1]
          main_program_name = self.findCMakeVarValue(cmakelists, cmake_var, dynamic=False)
        break
    return Path(main_program_name).name

  def findCMakeVarValue (self, cmakelists, cmake_var, dynamic=False, force_scan=True, **update_options):
    u"""
  
    .. note ::

      A CMake var defintion is a line like SET(cmake_var val) or OPTION(cmake_var)
      dynamic=False is quicker but very basic and can return bad answer 
      (variable changed in CMake command line, variable defined in subdirectory,
      ...). You shouldn't use it.
  
    :param cmakelists: path to main CMakeLists.txt file
    :type cmakelists: L{Path <pylot.core.path.Path}
    :param cmake_var: CMake variable name
    :type cmake_var: str
    :param dynamic: if True, run cmake to find CMake variable value if False, search 'cmake_var' definition in CMakeLists.txt file.
    :type dynamic: bool
    :param force_scan: if False, use last scan else scan sources again
    :type force_scan: bool

    :return: first CMake variable value found
    """
    if dynamic is False :
      f_cmakelists = Utils.readfile(cmakelists)
      gco = regexpr.gco_cmake_var_def(cmake_var) #gco : generated compiled regular expression
      for l in f_cmakelists:
        match_obj = gco.match(l)
        if match_obj :
          return l[len(match_obj.group()):].split()[0]
      return u'UnknownExecutableName'
    else :
      raise NotImplementedError

  def generateBuildPath(self, debug=False, build_name=None,
                        build_dir=None, src_dir=None):
    u"""
    Generates a "build" path in "build_dir" directory. 
    If build_name is None, name is generated automatically from src_dir 
    name and some build options (shared, debug). If build_dir is None,
    it uses ['builds'] pathbook key. If src_dir is None, uses ['src'] pathbook 
    key

    :param build_name: "build" directory name 
    :type build_name: str or None
    
    :param build_dir: directory containing all builds.
    :type src_dir: str, :class:`~pylot.core.Path` or None
    
    :param src_dir: source directory 
    :type src_dir: str or :class:`~pylot.core.Path`
    
    :return: build directory
    :rtype: :class:`~pylot.core.Path`
    """
    if build_dir is None :
      build_dir = self.pathbook(u'builds')
    else :
      build_dir = Path(build_dir).abspath()

    if build_name is None :
      if src_dir is None :
        src_dir = self.pathbook(u'src')
      else :
        src_dir = Path(src_dir).abspath()

      suffixes = [src_dir.name]

      shared = False
      if u'BUILD_SHARED_LIBS' in self.cmake_options :
        shared = self.cmake_options(u'BUILD_SHARED_LIBS')

      if debug :
        suffixes.append(u'debug')
      if shared :
        suffixes.append(u'shared')

      suffix = self.opt[u'general'](u'build_suffix')
      path = Path(build_dir,
        u'_'.join(suffixes) + unicode2(suffix))
    else :
      path = Path(build_dir, build_name)

    return path

  def cmdGroupCMakeScanSourceDir (self, csvfilename=u'cmake_options.default.csv',
                                  expert=False, options=[], cmake_book=None):
    u"""

    Creates a :class:`~pylot.core.CommandGroup` that scans ['src'] directory
    in order to find available cmake options. 
    It uses :command:`cmake -LA` scan command.

    :param csvfilename: A csv file containing cmake options description and values
    :type csvfilename: str or :class:`~pylot.core.Path`
    :param options: command line option to pass to cmake
    :type options: list
    :param cmake_book: pass cmake variables definitions
    :type cmake_book: :class:`~pylot.core.Book`
    """
    self._scansrc_vars = cmake_book
    workdir = mkdtempu()

    command_group = CommandGroup(u'CMakeScanSourceDir',
                                 self.tr_(u'Scan source directory'), u'Build')
    command_group.addCmd(self.bash.makeDir(workdir.abspath()))
    command_group.working_directory = workdir.abspath()

    # Add option to "cmake -LA" command
    # Example: CMAKE_MODULE_PATH
    if cmake_book is not None :
      for datum in cmake_book.values() :
        cmake_var = datum.toCMakeVarStr()
        if cmake_var : options.append(cmake_var)

    cmakeout = Path(workdir, u'scansrc.out')
    script_path = Path(workdir, u'script.sh')
    script = ShellScriptWriter()
    script.addCmd(u'cd %s' % workdir)
    script.addCmd(u'cmake -LA %s %s > %s' % (u' '.join(options),
                  self.pathbook(u'src').shellpath(), cmakeout.shellpath()),
                  main=True)
    script_path = script.toFile(script_path)
    script_path.chmod(0755)
    command_group.addShellCmd(script_path.shellpath(),
      alias=self.tr_(u'generate cmake option file'))

    self.created_paths.add(workdir)
    self.__scansrcpaths = workdir

    return command_group

  def scanSources (self, files=u'cmake_options.csv', cmake_book=None):
    u"""
    Scans source directory (['src']) and updates it with csv files 'files'
    You can pass cmake_book CMake options to scanner.
    
    :param files: list of CSV book files used to update cmake options (ex: description files containing tooltips and groups)
    :type files: `list`, `unicode` or :class:`~pylot.core.Path`
    :param cmake_book: book containing cmake options passed to scanner
    :type cmake_book: :class:`~pylot.core.Book`
    """
    if not is_iterable(files) :
      files = [files]
    if cmake_book is None :
      cmake_book = self.default_cmake_book


    # Create scansrc.out file
    # this file is a text file containing cmake options(CMake format)
    # ex: WITH_MODULE:BOOL = ON
    cg_scan_sources = self.cmdGroupCMakeScanSourceDir(cmake_book=cmake_book)
    self.process.run(cg_scan_sources, wait=True)

    # Clear manager options
    self.cmake_options.clear()

    # Open scansrc file
    workdir = self.__scansrcpaths
    cmakeout = Path(workdir, u'scansrc.out')
    outfile = Utils.readfile(cmakeout)

    # Parse scansrc file and convert it to Book
    for line in outfile :
      datum = CMakeOption.FromCMakeOptionStr(line)
      if datum :
        self.cmake_options.upFromDatum(datum)
    outfile.close()

    # Copy this new book to self._cmake_src_options
    self._cmake_src_options = copy.deepcopy(self.cmake_options)

    # Create a new book from csv files ("csv book")
    # This book may not match sources options
    cmake_options = CMakeOptionBook()
    for confdir in self.defaultconf :
      for conffile in files :
        cmake_options.upFromFile(Path(confdir, conffile))

    # force absolute path for special cmake variables
    for key in cmake_options :
      if cmake_options[key].abspath :
        cmake_options[key] = cmake_options(key).abspath()

    # Update existing source options with "csv book" options
    for key in cmake_options :
      # add only if yet in dict because options are defined by code sources
      if key in self.cmake_options :
        self.cmake_options[key].update(cmake_options[key],
                                       CMakeOption.getFieldsStd())

    # reset option passed to cmake scan.
    if self._scansrc_vars :
      for datum_id in self._scansrc_vars :
        self._cmake_src_options[datum_id] = None

    # Erase tmporary files
    self.__scansrcpaths.rmtree()

  def closeProject(self):
    self.cmake_options.clear()
    self.options_book.clear()
    self.options_book_expert.clear()

  def cmdGroupMake(self, target, build_dir):
    return cg_make(target, build_dir)

  def cmdGroupCMake (self, debug, build_name, cleancache, cmake_book=None,
                     src_dir=None, builds=None):
    u"""
    Creates a :class:`~pylot.core.CommandGroup` that configures project.
    (Adds cmake commands "cmake [OPTIONS]/path/to/src")
    
    .. seealso :: 
      :meth:`~pylot.core.DevelManager.generateBuildPath`
    """
    if cmake_book is None :
      cmake_book = self.default_cmake_book
    try:
      self._cmake_src_options
    except AttributeError :
      self.io.writeStderr(self.tr_(u'You must scan sources before running CMake\n'),
                          style=u'warn')
      raise IOError
    check_type(u'cmake_book', cmake_book, CMakeOptionBook, none=True)

    if debug is True :
      self.cmake_options[u'CMAKE_BUILD_TYPE'] = u'Debug'
    else :
      self.cmake_options[u'CMAKE_BUILD_TYPE'] = u''

    if src_dir is None :
      src_dir = self.pathbook(u'src')

    buildir = self.generateBuildPath(debug, build_name, builds, src_dir)
    self.pathbook[u'build'] = buildir

    cmds = []

    # Add cmake option which differ from default source cmake options
    cmake_options = self.cmake_options.keys()
    cmake_options.sort()
    for opt in cmake_options :
      datum = self.cmake_options[opt]
      add_option = False
      try :
        add_option = (self.cmake_options(opt) != self._cmake_src_options(opt))
      except KeyError :
        add_option = True
      else : pass

      cmake_var = datum.toCMakeVarStr()
      if cmake_var and add_option : cmds.append(cmake_var)

    # Add cmake options defined by user
    if cmake_book is not None :
      for datum in cmake_book.values() :
        cmake_var = datum.toCMakeVarStr()
        if cmake_var :
          cmds.append(cmake_var)

    cmake_vars = u' '.join(cmds)

    command_group = CommandGroup(u'CMake', u'CMake', u'Build')
    command_group.addCmd(self.bash.makeDir(buildir))
    command_group.working_directory = buildir
    if cleancache is True :
      command_group.addCmd(self.bash.remove(
                           Path(buildir, u'CMakeCache.txt'), force=True))
    command_group.addShellCmd(u'cmake', [cmake_vars, src_dir.shellpath()])
    return command_group

  def cmdGroupBuild (self, target=u'', debug=False, build_name=None,
                     cleancache=False, make=True):
    u"""
    Creates a :class:`~pylot.core.CommandGroup` that build (cmake & make) 
    program. 
    
    :param make: if True, launch "make target" after "cmake"
    :type make: bool

    .. seealso ::
      :meth:`~pylot.core.DevelManager.cmdGroupMake`,
      :meth:`~pylot.core.DevelManager.cmdGroupCMake`,
    """
    _cg_cmake = self.cmdGroupCMake(debug, build_name, cleancache)
    if make is True :
      build_dir = self.pathbook(u'build')
      _cg_make = cg_make(target, build_dir)
      cg_build = merge_command_groups([_cg_cmake, _cg_make])
      cg_build.category = u'Build'
      cg_build.group_id = u'build'
      cg_build.alias = u'CMake and Make'
      return cg_build
    else :
      return _cg_cmake

  def cmake (self, debug=False, cleancache=True, build_name=None,
             cmake_book=None):
    u"""
    Configures project using cmake.
    
    :param debug: build in debug mode
    :type debug: bool
    :param cleancache: remove CMakeCache before configure
    :type cleancache: bool
    :param build_name: build directory name. Build directory is created in pathbook['builds'] directory.
    :type build_name: str
    :param cmake_book: pass additionnal cmake options. cmake_book is a Book
    :type cmake_book: L{Book<pylot.core.Book>}
    """
    cg = self.cmdGroupCMake(debug=debug, build_name=build_name,
                            cleancache=cleancache, cmake_book=cmake_book)
    return self.process.run(cg)

  def make (self, target=u'', build_dir=None):
    u"""
    Launches "make target" on build_dir
    Ex: make('install', 'pylot-build') is equivalent to
    cd pylot-build && make install

    :param target: :program:`make` target ('', 'install', 'doc', 'clean', ...)
    :type target: str
    :param build_dir: build directory
    :type build_dir: str
    """
    if build_dir is None :
      build_dir = self.pathbook(u'build')
    cg = cg_make(target, build_dir)
    return self.process.run(cg)

  def build (self, target=u'', debug=None, cleancache=None):
    u"""
    Configures and builds project (cmake && make)

    :param target: make target
    :type target: str
    :param debug: build in debug mode. [Default: Manager value]
    :type debug: bool
    :param cleancache: remove CMakeCache before configure [Default: Manager value]
    :type cleancache: bool
    """
    if cleancache is None :
      cleancache = True
    if debug is None :
      debug = False

    try :
      self._cmake_src_options
    except AttributeError :
      self.updateCMakeOptions()
    cg = self.cmdGroupBuild(target=target, debug=debug, cleancache=cleancache)
    return self.process.run(cg)

  def eraseAndBuild (self, target=u'', debug=False,
    build_name=None, cleancache=False, force=False, make=True):
    u"""
    Like build but also removes build dir.
 
    :param force: if True, realy remove directory else return build path
    :type force: bool

    .. seealso ::

      :meth:`~pylot.core.DevelManager.build`
    """
    buildir = self.generateBuildPath(debug, build_name)
    if force is True :
      cg = CommandGroup(u'eraseAndBuild', category=u'Build')
      if buildir.exists() :
        if Path(buildir, u'.pylot') in buildir.listdir() :
          self.created_paths.add(buildir)
        cg.addCmd(self.bash.remove(buildir, force=force))
        cg.addCommandGroup(self.cmdGroupBuild(debug=debug,
          build_name=build_name, cleancache=cleancache, target=target))
      return self.process.run(cg)
    else :
      return buildir

  def updateCMakeOptions (self, source_dir=None, desc_files=None,
                          cmake_book=None, force_no_scan=False):
    u"""
    Scans source directory and updates 
    :attr:`~plugins.cmake.HandlerPart.cmake_options` attribute.

    To add informations like alias, tooltip, value list, ..., just pass CSV
    description files.
    
    Ex of description file.

    .. code-block:: none

      #name;nature;value;default_value;value_list;tooltip;group;alias
      WITH_OPENGL;bool;True;;;Use OpenGL for rendering;display;Use OpenGL
    
    :param source_dir: source directory [Default: current 'src' pathbook value]
    :type source_dir: str or :class:`~pylot.core.Path`
    :param desc_files: CMake Options CSV description file [Default: pylot default description file, basic]
    :type desc_files: str or :class:`~pylot.core.Path`
    """
    if source_dir is not None :
      old_dir = self.pathbook(u'src')
      self.pathbook[u'src'] = source_dir
      if self.pathbook(u'src') != old_dir :
        self.io.writeStatus(u"%s.pathbook['src'] changed to %s" % (
          self.__class__.__name__, self.pathbook(u'src')), style=u'warning')
    if desc_files is None :
      desc_files = u'cmake_options.csv'
    if force_no_scan is True :
      self.cmake_options.clear()
      self._cmake_src_options = copy.deepcopy(self.cmake_options)
    else :
      self.scanSources(files=desc_files, cmake_book=cmake_book)

  def loadCMakeOptions(self, csv_file=None):
    u"""
    Loads user cmake options

    :param csv_file: path to cmake option config file, if None, seek in 'userconf' dir
    :type csv_file: str, L{Path<pylot.core.path.Path>} or None
    """
    if csv_file is None :
      filename = u'cmake_options_%s.csv' % hash(self.pathbook(u'src'))
      src = Path(self.pathbook(u'cmakeoptions'), filename)
    else :
      src = Path(csv_file).abspath()

    cmake_options = CMakeOptionBook()
    cmake_options.upFromFile(src)
    msg = self.tr_(u'load CMake options from "%s"\n') % src.shortestpath()
    self.io.writeStatus(msg)
    for key in cmake_options :
      # add only if yet in dict because options are defined by code sources
      if key in self.cmake_options :
        self.cmake_options[key].update(cmake_options[key],
          mod_attrs=[u'value', u'value_list'])

  def saveCMakeOptions (self, dest=None):
    u"""
    Saves non expert cmake options in ['cmakeoptions'] directory.
    Cmake option file name is suffixed by source directory name 
    So options are linked to a project.
    """
    if dest :
      configfile = Path(dest)
    else :
      # Save hash/path dict for user
      dic_file = Path(self.pathbook(u'cmakeoptions'), u'hash_dict.csv')
      dic = Book(title=u'hash_dict')
      dic.importfile(dic_file, u'csv')
      dic[u'key_%s' % hash(self.pathbook(u'src'))] = self.pathbook(u'src')
      dic.exportfile(dic_file, u'csv')

      filename = u'cmake_options_%s.csv' % hash(self.pathbook(u'src'))
      configfile = Path(self.pathbook(u'cmakeoptions'), filename)

    cmake_options = CMakeOptionBook()
    for datum in self.cmake_options.values() :
      if datum.level != u'expert' :
        cmake_options[datum.name] = datum
    cmake_options.exportfile(configfile, fmt=u'csv')
    msg = self.tr_(u'Save CMake options in "%s"\n') % configfile.shortestpath()
    self.io.writeStatus(msg)

  def splitCMakeOptions(self, book):
    self.options_book.clear()
    self.options_book_expert.clear()
    for dat in book.values():
      if dat.level == u'expert' :
        self.options_book_expert.upFromDatum(dat)
      else:
        self.options_book.upFromDatum(dat)

  def _translate(self):

    self._setTexts([
      (self.options_book, self.tr_(u'CMake Options')),
      (self.options_book_expert, self.tr_(u'CMake Options (Expert)')),
      (self._cmake_options, self.tr_(u'CMake Options')),
      (self._cleancache, self.tr_(u'Clean CMake Cache')),
      ])

    self._setTexts([
      (self._cmake_options, self.tr_(u'CMake options database')),
      (self._cleancache, self.tr_(u'if True, removes CMake cache (CMakeCache.txt)')),
      ], field=u'tooltip')

  def _readSettings(self, settings, common):
    settings.beginGroup(u'pathbook')
    builds = self.pathbook[u'builds']
    cmakeoptions = self.pathbook[u'cmakeoptions']
    builds.userUpgrade(settings.getValue(u'builds', builds))
    cmakeoptions.userUpgrade(settings.getValue(u'cmakeoptions', cmakeoptions))
    settings.endGroup()

    settings.beginGroup(u'general')
    self.cleancache = settings.getValue(u'cleancache', self.cleancache).value
    self.debug_mode = settings.getValue(u'debug_mode', self.debug_mode).value
    build_suffix = self.opt[u'general'][u'build_suffix']
    build_suffix.userUpgrade(settings.getValue(u'build_suffix', build_suffix))
    settings.endGroup()

  def _writeSettings(self, settings, common):
    settings.beginGroup(u'pathbook')
    settings.setValue(u'builds', self.pathbook[u'builds'])
    settings.setValue(u'cmakeoptions', self.pathbook[u'cmakeoptions'])
    settings.setValue(u'src', self.pathbook[u'src'])
    settings.endGroup()
    settings.beginGroup(u'general')
    settings.setValue(u'cleancache', self.cleancache)
    settings.setValue(u'debug_mode', self.debug_mode)
    settings.setValue(u'build_suffix', self.opt[u'general'][u'build_suffix'])
    settings.endGroup()

  def _quit(self):
    pass

if __name__ == '__main__':
  u"""
  Example of script
  """
